// Copyright (c) 2013-2015 David Muse
// See the COPYING file for more information.

#include <wikitext/wikitext.h>
#include <rudiments/character.h>
#include <rudiments/charstring.h>
#include <rudiments/sys.h>

#include <config.h>

class wikitextpreprocessor : public compilerpreprocessor {
	public:
			wikitextpreprocessor();
		bool	process(const char *input,
				stringbuffer *output,
				xmldomnode *metadata);
	private:
		const char	*isList(const char *input);
		bool		includeFile(const char *pos,
						const char **outpos,
						stringbuffer *output);
};


wikitextpreprocessor::wikitextpreprocessor() {
	id="wikitext";
}

bool wikitextpreprocessor::process(const char *input,
					stringbuffer *output,
					xmldomnode *metadata) {

	// run through the input...
	const char	*c=input;
	bool		atlinestart=true;
	bool		inpre=false;
	bool		inblockquote=false;
	while (*c) {

		// detect pre-formatted text processor
		if ((!charstring::compare(c,"{{{",3) &&
				(charstring::compare(c,"{{{#",4) &&
				charstring::compare(c,"{{{\n#",5) &&
				charstring::compare(c,"{{{\r\n#",6))) ||
			!charstring::compare(c,"{{{#!code",9) ||
			!charstring::compare(c,"{{{\n#!code",10) ||
			!charstring::compare(c,"{{{\r\n#!code",11)) {
			inpre=true;
		} else if (inpre && !charstring::compare(c,"}}}",3)) {
			inpre=false;
		}

		// wrap indented sections with blockquote processors
		// and remove leading spaces...
		if (!inpre && atlinestart) {
			if (*c==' ' || *c=='	') {

				// unless the spaces are indenting a list
				const char	*end=isList(c);
				if (end) {
					output->append(c,end-c);
					c=end;
					continue;
				}

				if (!inblockquote) {
					output->append("{{{#!blockquote\n");
					inblockquote=true;
				}
				c++;
				continue;

			} else if (inblockquote &&
					*(c-1)!=' ' && *(c-1)!='	') {

				output->append("}}}\n");
				inblockquote=false;
				continue;
			}
		}

		// include files surrounded by @...@
		if (atlinestart && *c=='@') {
			if (!includeFile(c,&c,output)) {
				output->append(*c);
				c++;
			}
			continue;
		}

		// convert various end-of-lines into \n's
		if (!charstring::compare(c,"\r\n",2) ||
			!charstring::compare(c,"\n\r",2)) {
			output->append('\n');
			c=c+2;
			atlinestart=true;
			continue;
		}
		if (*c=='\r' || *c=='\n') {
			output->append('\n');
			c++;
			atlinestart=true;
			continue;
		}

		atlinestart=false;

		// normalize tables...

		// slashes can be used at the end of a line to indicate that
		// the next row of the table should actually be part of this row
		// if we find ||(whitespace)\(whitespace)(eol)(whitespace)||
		// then skip all the way to the next ||
		if (!inpre && !charstring::compare(c,"||",2)) {

			// skip whitespace after ||
			const char *c1=c+2;
			while (*c1 && (*c1==' ' || *c1=='	')) {
				c1++;
			}

			// if we find a slash
			if (*c1=='\\') {

				// skip whitespace after slash
				c1++;
				while (*c1 && (*c1==' ' || *c1=='	')) {
					c1++;
				}
			
				// if we find an end of line then skip it
				bool	eolfound=false;
				if (!charstring::compare(c1,"\r\n",2) ||
					!charstring::compare(c1,"\n\r",2)) {
					c=c1+2;
					eolfound=true;
				} else if (*c1=='\n' || *c1=='\r') {
					c=c1+1;
					eolfound=true;
				}
				if (eolfound) {

					// skip whitespace after eol
					while (*c1 && (*c1==' ' ||
							*c1=='	')) {
						c1++;
					}

					// if we find || then set the current
					// position to that
					if (!charstring::compare(c1,"||",2)) {
						c=c1;
					}
				}
			}
		}

		// convert table headers and cells
		bool	foundheader=!charstring::compare(c,"||=",3);
		if (!inpre && (foundheader || !charstring::compare(c,"||",2))) {

			// start header/cell
			if (foundheader) {
				output->append("{{{#!th\n");
			} else {
				output->append("{{{#!td\n");
			}

			// output until =|| or ||
			c=c+((foundheader)?3:2);
			while (*c) {
				if (foundheader &&
					!charstring::compare(c,"=||",3)) {
					c++;
					break;
				} else if (!charstring::compare(c,"||",2)) {
					break;
				}
				output->append(*c);
				c++;
			}

			// close header/cell
			output->append("\n}}}\n");

			// skip whitespace after ||
			const char	*c1=c+2;
			while (*c1 && (*c1==' ' || *c1=='	')) {
				c1++;
			}
			
			// if we find an end of line then this
			// is the end of the row
			if (*c1=='\n' || *c1=='\r') {

				// we also need to determine whether this
				// is the end of the table or just the end
				// of a row though...

				// skip eol
				if (!charstring::compare(c1,"\r\n",2) ||
					!charstring::compare(c1,"\n\r",2)) {
					c1=c1+2;
				} else {
					c1++;
				}

				// set the current character pointer
				// to after the eol
				c=c1;

				// skip whitespace after eol
				while (*c1 && (*c1==' ' || *c1=='	')) {
					c1++;
				}

				// if there are more cells then output a table
				// row marker
				if (!charstring::compare(c1,"||",2)) {
					output->append("|-");
				}

				output->append("\n");
			}

			continue;
		}

		// append the character to the output
		output->append(*c);

		// move on
		c++;
	}

	// if we're still in a blockquote, then end it
	if (inblockquote) {
		output->append("}}}\n");
	}

	return true;
}

bool wikitextpreprocessor::includeFile(const char *pos,
					const char **outpos,
					stringbuffer *output) {

	// skip the initial @
	pos++;

	// find the end of the line
	bool		found=false;
	stringbuffer	fname;
	while (*pos && *pos!='\r' && *pos!='\n') {
		if (*pos=='@' &&
			(!(*pos) || *(pos+1)=='\r' || *(pos+1)=='\n')) {
			found=true;
			pos++;
			break;
		}
		fname.append(*pos);
		pos++;
	}
	if (!found || !file::exists(fname.getString())) {
		return false;
	}
	
	char	*fc=file::getContents(fname.getString());
	output->append(fc);
	delete[] fc;

	*outpos=pos;
	return true;
}

const char *romanset[]={
	"i.",
	"ii.",
	"iii.",
	"iiii.",
	"iiiii.",
	"iiiiii.",
	NULL
};

const char *wikitextpreprocessor::isList(const char *input) {
	const char *ch=input;
	while (*ch && (*ch==' ' || *ch=='	')) {
		ch++;
	}
	if (*ch=='*' || *ch=='-' || *ch=='#' ||
		(character::isAlphanumeric(*ch) && *(ch+1)=='.') ||
		(charstring::inSet(ch,romanset))) {
		return ch;
	}
	return NULL;
}


wikitext::wikitext() {
	inputgrammar=NULL;
	outputgrammar=NULL;
	debuglevel=0;
	fmt=NULL;
	fmtparams=NULL;
	wtpre=NULL;
	dirty=true;

	addMacroHandler("image");
}

wikitext::~wikitext() {
	clearMacroHandlers();
	delete[] inputgrammar;
	delete[] outputgrammar;
	delete wtpre;
}

void wikitext::addMacroHandler(const char *macrohandler) {
	macrohandlers.append(charstring::duplicate(macrohandler));
	dirty=true;
}

void wikitext::clearMacroHandlers() {
	for (linkedlistnode< char * > *node=macrohandlers.getFirst();
					node; node=node->getNext()) {
		delete[] node->getValue();
	}
	macrohandlers.clear();
	dirty=true;
}

void wikitext::setOutputFormat(const char *format, const char *parameters) {
	fmt=format;
	fmtparams=parameters;
	dirty=true;
}

bool wikitext::reformat(const char *input, stringbuffer *output) {

	errorstr.clear();

	// initialize everything if this is the first run,
	// or if something has changed
	if (dirty) {
		if (!initialize()) {
			return false;
		}
		dirty=false;
	}

	// set debug level
	c.setDebugLevel(debuglevel);

	// compile
	if (!c.compile(input,output)) {
		errorstr.append(c.getError());
		return false;
	}

	// success
	return true;
}

void wikitext::setDebugLevel(uint8_t debuglevel) {
	this->debuglevel=debuglevel;
}

bool wikitext::initialize() {

	// load the input grammar
	stringbuffer	grammarfilename;
	grammarfilename.append(WIKITEXT_GRAMMARDIR);
	grammarfilename.append(sys::getDirectorySeparator());
	grammarfilename.append("wikitext.xml");
	delete[] inputgrammar;
	inputgrammar=file::getContents(grammarfilename.getString());
	if (charstring::isNullOrEmpty(inputgrammar)) {
		errorstr.append("failed to load input grammar");
		return false;
	}

	// load the output grammar
	grammarfilename.clear();
	grammarfilename.append(WIKITEXT_GRAMMARDIR);
	grammarfilename.append(sys::getDirectorySeparator());
	grammarfilename.append(fmt)->append(".xml");
	delete[] outputgrammar;
	outputgrammar=file::getContents(grammarfilename.getString());
	if (charstring::isNullOrEmpty(outputgrammar)) {
		errorstr.append("failed to load output grammar");
		return false;
	}

	// set input/output grammars
	c.setInputGrammar(inputgrammar,"doc");
	c.setOutputGrammar(outputgrammar);

	// set module search path
	c.clearModulePaths();
	c.appendModulePath(WIKITEXT_LIBEXECDIR);

	// clear (pre/post)processors
	c.clearPreProcessors();
	c.clearProcessors();
	c.clearPostProcessors();

	// load wikitext pre-processor
	delete wtpre;
	wtpre=new wikitextpreprocessor();
	c.appendPreProcessor(wtpre);

	// build formatter module name
	stringbuffer	formatfilename;
	formatfilename.append("wikitextformat_");
	formatfilename.append(fmt)->append('.');
	formatfilename.append(WIKITEXT_MODULESUFFIX);

	// load formatter pre-processor
	stringbuffer	classname;
	classname.append(fmt)->append("_preprocessor");
	c.appendPreProcessor(formatfilename.getString(),
					classname.getString(),
					"formatter",fmtparams);

	// load macro handlers
	stringbuffer	macrofilename;
	for (linkedlistnode< char * > *node=macrohandlers.getFirst();
						node; node=node->getNext()) {

		const char	*macrohandler=node->getValue();

		macrofilename.clear();
		macrofilename.append("wikitextmacrohandler_");
		macrofilename.append(macrohandler)->append('.');
		macrofilename.append(WIKITEXT_MODULESUFFIX);

		c.appendProcessor(macrofilename.getString(),
					macrohandler,macrohandler,
					(xmldomnode *)NULL);
	}

	// load formatter processor
	classname.clear();
	classname.append(fmt)->append("_processor");
	c.appendProcessor(formatfilename.getString(),
					classname.getString(),
					"formatter",fmtparams);

	// load formatter post-processor
	classname.clear();
	classname.append(fmt)->append("_postprocessor");
	c.appendPostProcessor(formatfilename.getString(),
					classname.getString(),
					"formatter",fmtparams);

	return true;
}

const char *wikitext::getError() {
	return errorstr.getString();
}
